#!/bin/sh
#\
for x in 8.7 8.6 8.5 ""; do \
  command -v "tclsh$x" >/dev/null && exec "tclsh$x" "$0" "$@"; \
  command -v "wish$x" >/dev/null && exec "wish$x" "$0" -- "$@"; \
done

# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
# FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

# NAME
#      bokpass - decode/encode passwords from/for boktai games
#
# SYNOPSIS
#      bokpass [-gui] [-kbd] [-invalid] [-columns columns] [-fontsize size]
#      bokpass [-cli] [-verbose] -game game password ...
#
# DESCRIPTION
#      bokpass is a tool to decode and encode passwords from/for the Boktai
#      series of games, including Lunar Knights.
#
# OPTIONS
#      -gui    Open the GUI, this is the default when there are no
#              arguments provided.
#
#      -columns columns
#              Arrange the gui in specified number of columns,
#              the default is 2.
#
#      -fontsize size
#              Set the default font size to `size', the password entry and
#              virtual keyboard font sizes are an additional 20% bigger.
#              The default is 10.
#
#      -cli    Decode the provided passwords, this is the default when
#              arguments are provided.
#
#      -verbose
#              If a value has a single string representation, print that
#              next to its numerical representation.
#
#      -invalid
#              Do not validate the data fields.
#
#      -game game
#              The game that the provided password(s) are for, `game' can
#              be a number (1-4), the title of the game, or the short-name
#              for the game (zoktai/shinbok).
#              This option is required for decoding passwords.
#
# GUI
#      The tabs at the top select the game.
#
#      Under that is the encode button "↱", the password text box, the
#      cycle button "↻", and the decode button "↴".
#
#      The cycle button "↻" edits the password to change the `offset'
#      field and change how it would be decoded.  For if one of the first
#      letters was a typo.  Though note that the `offset' is not included in
#      Boktai 1's checksum.
#
#      Then columns (changed by the `-columns' option) of labeled dropdown
#      menus, number-boxes, checkboxes, and text fields that correspond to
#      the various fields of the selected game's password.
#
#      Then there is an error line, usually containing "✓", that will show
#      an error preceded by "⚠" if there was an encoding error, a decoding
#      error, or if the password may be considered invalid.  For example,
#      Boktai 2 (US) will not accept Boktai 1 passwords that have 0 playtime
#      or clears, and will not accept Japanese characters in the player's
#      name.
#
#      If the `-kbd' flag is given then at the bottom there are tabs
#      containing grids of buttons that function as a virtual keyboard.
#      When clicking a text-entry field it will automatically switch to
#      the appropriate keyboard tab, and lock it out of any invalid tabs.
#
# BOKTAI PASSWORD
#      JP    EN
#      u3    u3    region
#      u16   u16   checksum
#      u2    u2    offset       [0x0203C800, ([0x03004620]+1 & 0x3FF)*2] & 3
#      u9    u9    sol/4        [0x0203D8BC] >> 12
#      u6    u7    timezone     [0x0203D82C]
#      u6    u6    hours        [0x0203D8FC] / 60 / 60 / 60
#      u6    u6    minutes      [0x0203D8FC] / 60 / 60 % 60
#      u3    u3    difficulty   [0x0203D838]
#      u5    u5    dungeons     [0x0203D8A6]h
#      u3    u3    clears       [0x0203D8B6]h
#      u4    u4    continues    [0x0203D902]h
#      u5    u5    caught       [0x0203DB0C]h
#      u7    u7    kills        [0x0203D8B0]h
#      u4    u4    rank         [0x0203D8C6]h
#      u5    u5    title        [0x0203D828]h
#      u8[5] u8[9] name         [0x0203D830]...
#      u6    u6    link-battles [0x0203DD2E]
#      u6    u6    link-trades  [0x0203DD32]
#      u8    u8    loan         [0x0203D878]
#      u0    u15   padding      0
#
#      The checksum offset is 24-bits and uses the constant 0x1021.
#      The encoding offset is 24-bits and uses the initializer 0x8C159.
#
# BOKTAI 2 PASSWORD
#      JP    EN
#      u16   u16   checksum
#      u2    u2    padding      0
#      u3    u3    region
#      u3    u3    offset       [0x0203B400, ([0x030046B8]+1 & 0x3FF)*2] & 7
#      u6    u7    timezone     [[0x030047A8], 0x20]
#      u3    u3    side         [[0x030046A0], 0x2F8]h
#      u4    u4    style        [[0x030046A0], 0x2FA]h
#      s8    s8    kills/8      [[0x030046A0], 0x1F0]h / 8
#      u7    u7    forge        [[0x030046A0], 0x2FC]h
#      u6    u6    link-battles [[0x030046A0], 0x914]h
#      u6    u6    link-shops   [[0x030046A0], 0x918]h + [[0x030047A8], 0x91A]h
#      s10   s10   sol/4096     [[0x030046A0], 0x1D8] / 4096
#      s8    s8    loan/256     [[0x030046A0], 0x1D4] / 256
#      u6    u6    hours        [[0x030046A0], 0x2B4] / 60 / 60 / 60
#      u16   u16   titles       [[0x030046A0], 0x1EE]h
#      u8[5] u8[9] name         [[0x030046A0], 0x3C0]b...
#      u0   u15    padding      0
#      [0x030046A0] is heap allocated, typically 0x0203C400.
#
#      The checksum offset is 18-bits and uses the constant 0x1021.
#      The encoding offset is 24-bits and uses the initializer 0xB8E6E.
#
# BOKTAI 3 PASSWORD
#      u16   checksum
#      u2    padding      0
#      u3    region
#      u3    offset       [0x203B400, ([0x03005308] + 1 & 0x3FF) * 2] / 256 & 7
#      u6    timezone     [[0x30053F8], 0x18]
#      u8    kills/8      [[0x2000710], 0x538]h / 8
#      u7    forges       [[0x2000710], 0x664]h
#      u9    races        [[0x2000710], 0x75A]h
#      u7    link-races   [[0x2000710], 0x7B4]h
#      u1    cross-linked [[0x2000710], 0x75F]b
#      u6    endings      [[0x2000710], 0x75E]b
#      u10   sol/4096     [[0x2000710], 0x520]
#      u8    loan/256     [[0x2000710], 0x51C]
#      u6    hours        [[0x2000710], 0x614] / 60 / 60 / 60
#      u12   titles       [[0x2000710], 0x536]h
#      u8[5] name         [[0x2000710], 0x781]b...
#      u0    padding      0
#      [0x02000710] is heap allocated, typically 0x0203C400.
#
#      The checksum offset is 18-bits and uses the constant 0x8005.
#      The encoding offset is 24-bits and uses the initializer 0x8C159.
#
# CAVEATS
#      I was unable to find a Shinbok password with what I labeled as the
#      "Cross-Linked" flag set.  I labeled it that way because it seems to
#      only be modified in bytecode function 1657 at file offset 0xDF2D81
#      which references a string 9797 that translates as a wireless-adapter
#      communication error, and it seems as though the wireless-adapter is
#      only used for the crossover battles.
#
# DISASSEMBLY NOTES
#     Password generation functions:
#         Boktai 1 JP put 1: 0x12D2E4
#         Boktai 1 NA put 1: 0x12E7D4
#         Boktai 1 EU put 1: 0x12B244
#         Boktai 2 JP put 2: 0x12DC0
#         Boktai 2 NA put 2: 0x12C14
#         Boktai 2 EU put 2: 0x12ABC
#         Boktai 3 JP put 3: 0x44E6C
#         Boktai 3 JP put 2: 0x451BC
#     Password reading functions:
#         Boktai 2 JP get 1: 0x112B4
#         Boktai 2 NA get 1: 0x111E0
#         Boktai 2 EU get 1: 0x10FC8
#         Boktai 3 JP get 3: 0x450D8
#         Boktai 3 JP get 2: 0x453DC
#
#     Boktai 1 uses a static random number table of 1024 numbers at 0x203C800;
#     the index stored at 0x3004620 with an inital value of 837.  The index
#     is only incremented when used (particle effects and enemies, mostly),
#     and is not stored in the save file.
#
#     This means, for example, you can just choose what emblem you want at the
#     Azure Sky Tower.  Entering the outside screen while there is still the
#     ice/fire increments the RNG by 1, entering Azure Sky Tower itself
#     increments it by two, and the steam/smoke increments it by 85.
#     So before collecting an emblem repeatedly running between the outside
#     of the tower and the 'Byroad of the Beasts' will increment the RNG by 1,
#     and repeatedly exiting/entering the tower will increment the RNG by 3
#     each cycle.  The following table tells you what actions after loading a
#     file saved on the 'Byroad of the Beasts' will get which emblem:
#         RNG Index  Byroad in/outs  AZT in/outs  Emblem
#            925           0             0        Earth
#            928           0             1        Cloud
#            930           2             1        Frost
#            931           0             2        Cloud
#            932           1             2        Frost
#            934           0             3        Cloud
#            937           0             4        Frost
#            939           2             4        Flame
#            940           0             5        Earth
#            943           0             6        Earth
#            946           0             7        Flame
#
#     Boktai 1's titles are checked in bytecode function 37607 in this order:
#     0  Trigger of Sol  S Rank and all 42 parts
#     1  Gun Master      Level 3 lenses and all 42 parts
#     6  Death           100 or more Continues
#     5  Berserker       Caught 400 or more times
#     3  Bishop          500 or more Kills
#     4  Queen           100 or less Kills
#     2  Gladiator       A win rate of 70% or more with at least 50 Link Battles
#     10 Running Boy     Punished 8 or more times
#     9  Solar Merchant  50 or more Link Trades
#     7  Solar Boy       600 or more Sol
#     8  Dark Boy        100 or less Sol
#     11 King            S Rank
#     12 Rook            A-/A/A+ Rank
#     13 Knight          B-/B/B+ Rank
#     14 Pawn            C-/C/C+ Rank
#     14 Pawn            Default
#
#     Boktai 2's "side" is calculated as follows:
#         128*r/max(r+b, 1) > 70 ? 0 : 128*r/max(r+b, 1) > 58 ? 2 : 1
#     Where `r' is the number of frames of "red" Django action and `b' is
#     the number of frames of "black" Django action.
#
#     Boktai 2's "style" is based on which weapon had the highest number of
#     frames in use rounded down to the nearest 15 minutes, or "No Style"
#     if there was a tie.
#
#     Boktai 3's endings should be:
#     1       Otenko lives.
#     2       Both Otenko and Sabata live.
#     4       Neither Otenko or Sabata live.
#     8       Sabata lives.
#
#     The checksums are CRC-16s initialized to 0xFFFF and with the result
#     inverted.  The polynomial 0x1021 is used in the first two games, 0x8005
#     is used in the third, and 0x180D is used in the fourth game.
#
#     The encoding algorithm is to initialize a value depending on the game,
#     and initialize the an accumulator variable to 0xFFFF.  For each iteration
#     the current value by 0x6262C05D and increment it by 1, then xor the result
#     into the xor accumulator.  This iteration in run a number of times
#     depending on the offset and 4 additional times, then before each iteration
#     starting at offset 4 until the end of the list the last 6-bits of the xor
#     accumulator are xored with the current 6-bit value.
#     Because of this truncation only the last 6 bits of any constant matters.
#     The initialization constants are 0x8C159 for games 1 and 3, 0xB8E6E for
#     game 2, and 0x5BB15 for game 4.
#
# BYTECODE NOTES
#     In Boktai 3 most things about the bytecode are the same as described
#     in github.com/Prof9/SolDec, though there are more 16-bit (control)
#     operators.  The Boktai 3 function that executes them is at 0x21AC6C.
#
#     The following are tables some useful addresses and where they are
#     found in RAM.
#
#     note: Addresses starting with 0x08 indicate a location in the ROM.
#          RAM-Address  Func-Table  Func-Count  BC-Offset   BC-Start
#     JP1  0x03000648   0x08E1D738  0x13F6      0x08E52328  0x08EE9F17
#     NA1  0x03000648   0x08E0DA88  0x140A      0x08E41D4C  0x08EDA36F
#     EU1  0x03000650   0x08DE2FBC  0x140F      0x08EC1350  0x08F5D001
#     JP2  0x03000748   0x08CBF1A0  0x2D13      0x08D1337C  0x08DA9DB4
#     NA2  0x03000748   0x08C8B56C  0x390A      0x08CE4B9C  0x08D7BBF5
#     EU2  0x03000748   0x08CA1E8C  0x3F48      0x08E2F0EC  0x08ECC13C
#     JP3  0x02000438   0x08D5A860  0x4260      0x08DD6BA0  0x08E5F048
#
#     In Boktai 1 the Func-Table is indexed like:
#         for (i = 0; i < Func-Count; i++)
#             if (Func-Table[i*2] == index)
#                 return BC-Offset + (Func-Table[i*2+1] & 0x00FFFFFF)
#
#     In others the Func-Table is indexed like:
#         BC-Offset + (Func-Table[(index & 0x7FFFFFFF) - 1] & 0x00FFFFFF)
#
#          RAM-Address  ???         String-Table  String-Data
#     JP1  0x03000658   0x08E276F0  0x08E27700    0x08E2B72C
#     NA1  0x03000658   0x08E17AE0  0x08E17AF0    0x08E1BE1C
#     EU1  0x03000660   0x08DED03C  0x08DED04C    0x08E00E64
#     JP2  0x03000758   0x08CCA5F0  0x08CCA600    0x08CD1594
#     NA2  0x03000758   0x08C99998  0x08C999A8    0x08CA0A80
#     EU2  0x03000758   0x08CB1BB0  0x08CB1BC0    0x08CD3A58
#     JP3  0x02000448   0x08D6B1E4  0x08D6B1F8    0x08D74DDC
#
#     The String-Table is indexed like:
#         String-Data + (String-Table[index] & 0x7FFFFFFF)
#     The strings are nul-terminated.
#
#     The current floor of the Azure Sky Tower is stored at memory location
#     0x203E8DD, and the number of clears at 0x203E8E0.  So building it up
#     can be skipped by changing 0x203E8E0 to 29+, and most of the climb up
#     can be skipped by changing 0x203E8DD to 99 after entering floor 1.
#
#     Boktai 1 -> 2 password effects (byte-code function JP #25 and NA/EU #28):
#     Trigger of Sol Start with Stats +1, a Healer, and a Magic Potion
#     Gun Master     Start with Stats +1, a Healer, and a Magic Potion
#     Gladiator      Start with Stats +1, and 2 Healers
#     Bishop         Start with Vitl +2, Strg +2, an Antidote, and an Elixer
#     Queen          Start with Sprt +2, Agil +2, a Speed, and Tiptoe Nut
#     Berserker      Start with Strg +2, a Tasty, and Rotten Meat
#     Death          Start with Vitl +2, a Solar, and Rotten Nut
#     Solar Boy      Start with Vitl +2, Sprt +2, a Solar Nut, and a Drop of Sun
#     Dark Boy       Start with Strg +2, Agil +2, a Power Nut, and a Bearnut
#     Solar Merchant Start with Sprt +2, Strg +2, a Redshroom, and a Blueshroom
#     Running Boy    Start with Vitl +2, Agil +2, and 2 Magic Potions
#     King           Start with Stats +1, an Elixer, and a Healer
#     Rook           Start with Vitl +1, Sprt +1, a Healer, and an Antidote
#     Knight         Start with Strg +1, Agil +1, an Earthly, and Solar Nut
#     Pawn           Start with an Earthly and Solar Nut
#     Invalid        Start with 2 Earthly Nuts
#
#     Boktai 2 -> 3 password effects (byte-code function #546):
#     Sword Master   Starting Strength + 1
#     Spear Master   Starting Spirit + 1
#     Hammer Master  Starting Vitality + 1
#     Fist Master    Starting Strength + 1
#     Gun Master     Starting Spirit + 1
#     Adept          Starting Vitality + 1
#     Day Walker     Start with a Sun card
#     Adventurer     Start with a Speed Nut
#     Agent          Start with a Tiptoe Nut
#     Collector      Start with a See-All Nut
#     Dark Hunter    Start with a Tasty Meat
#     Grand Master   Start with a Moon card
#     Red (Side)     Solar Trance time + 5000 frames
#     Black (Side)   Dark Trance time + 5000 frames
#     No Password    Start with 2 extra Earthly Nuts
#
# ACKNOWLEDGMENTS
#     Most of the Lunar Knight / Boktai DS information, and the character
#     tables were copied from github.com/Prof9/LKPassDecode.
#
#     github.com/Prof9/SolDec was very useful in understanding bytecode.
#
#     The Boktai 3 translations and Japanese timezone names are from
#     github.com/moozilla/boktai3trans.

# lmap shim for Tcl 8.5
if {[info commands lmap] == ""} {
  proc lmap {args} {
    if {[llength $args] < 3 || [llength $args] % 2 != 1} {
      return -code error \
        {wrong # args: should be "lmap vars list ?vars list ...? body"}
    }
    set i 0
    for {set i 0} {$i < [llength $args] - 1} {incr i 2} {
      for {set j 0; set l {}} {$j < [llength [lindex $args $i]]} {incr j} {
        upvar [lindex $args $i $j]  var$i,$j
        lappend l var$i,$j
      }
      lset args $i $l
    }
    set r {}
    foreach {*}[lrange $args 0 end-1] {
      lappend r [uplevel 1 [lindex $args end]]
    }
    return $r
  }
}

# dict map shim for Tcl 8.5
if {[namespace which -command ::tcl::dict::map] == ""} {
  proc ::tcl::dict::map {vars dict body} {
    if {[llength $vars] != 2} {
      return -code error {must have exactly two variable names}
    }
    upvar 1 [lindex $vars 0] key [lindex $vars 1] value
    ::set r {}
    dict for {key value} $dict {
      ::set value [uplevel 1 $body]
      set r $key $value
    }
    return $r
  }
  namespace ensemble configure ::dict -map [dict replace \
      [namespace ensemble configure ::dict -map] map ::tcl::dict::map]
}

namespace eval bok {namespace export *}

set bok::strings {en {
  games {
    jp1 {Bokura no Taiyō}
    jp2 {Zoku Bokura no Taiyō: Taiyō Shōnen Jango}
    jp3 {Shin Bokura no Taiyō: Gyakushū no Sabata}
    jp4 {Bokura no Taiyō: Django & Sabata}
    en1 {Boktai: The Sun Is in Your Hand}
    en2 {Boktai 2: Solar Boy Django}
    en3 {Boktai 3: Sabata's Counterattack}
    en4 {Lunar Knights}
  }
  labels {
    game-tabs {
      1  Boktai              2 {Boktai 2 / Zoktai}
      3 {Boktai 3 / Shinbok} 4 {Lunar Knights / Boktai DS}
    }
    keyboard-tabs {
      en64  Base64   lk64 {Base64 LK} jp64 {Base64 JP}
      en    English  ext  {Latin Ext.}
      hiri  Hirigana kata  Katakana
    }
    padding       Padding       header-padding  Padding
    region        Region        region-name     Region
    checksum      Checksum      offset          Offset
    sol           Sol           sol/4           Sol          sol/4096 Sol
    loan          Loan          loan/256        Loan
    timezone      Timezone      timezone-name   Timezone
    hours         Hours         minutes         Minutes
    difficulty    Difficulty    difficulty-name Difficulty
    dungeons      Dungeons      clears          Clears
    continues     Continues     caught          Caught
    kills         Kills         kills/8         Kills
    rank          Rank          rank-name       Rank
    forges        Forges        races           Races
    link-battles  Link-Battles  link-trades     Link-Trades
    link-shopping Link-Shopping
    link-races    Link-Races    cross-linked    Cross-Linked
    title         Title         title-name      Title
    titles        Titles        title-list      Titles
    side          Side          side-name       Side
    style         Style         style-name      Style
    sword         Sword         sword-name      Sword
    gun           Gun           gun-name        Gun
    terrennial    Terrennial    terrennial-name Terrennial
    climate       Climate       climate-name    Climate
    endings       Endings       ending-list     Survivors
    name-dark     Name(dark)    name-solar      Name(sol)     name    Name
  }
  regions {{Unknown Region #0} Japan {North America} Europe}
  timezones {jp {
    {Unknown Timezone #0}
    Ibaraki   Tochigi      Gunma    Saitama
    Chiba     Tokyo        Kanagawa Ogasawara
    Niigata   Toyama       Ishikawa Fukui
    Yamanashi Nagano       Gifu     Shizuoka
    Aichi     Mie          Shiga    Kyoto
    Osaka     Hyougo       Nara     Wakayama
    Tottori   Shimane      Okayama  Hiroshima
    Yamaguchi Tokushima    Kagawa   Ehime
    Kouchi    Fukuoka      Saga     Nagasaki
    Kumamoto  Ooita        Miyazaki Kagoshima
    Okinawa   Ishigakijima Sapporo  Hakodate
    Asahikawa Kushiro      Obihiro  Aomori
    Iwate     Miyagi       Akita    Yamagata
    Fukushima
  } na {
    {Unknown Timezone #0}
    {St. John's}  {Labrador City}  Halifax          Quebec
    Montreal       Ottawa          Toronto          Timmins
    Boston         Albany          Syracuse        {New York}
    Philadelphia   Pittsburgh     {Washington D.C.} Norfolk
    Raleigh        Charlotte       Atlanta          Jacksonville
    Tampa          Miami           Detroit          Cleveland
    Columbus       Lexington      {Thunder Bay}     Winnipeg
    Regina         Thompson        Indianapolis     Chicago
    Milwaukee      Minneapolis     Bismark         {St. Louis}
    Nashville      Memphis         Montgomery       Jackson
    {New Orleans} {Des Moines}     Lincoln         {Kansas City}
    Topeka         Springfield    {Little Rock}    {Oklahoma City}
    Dallas         Houston         Edmonton         Calgary
    Yellowknife    Denver          Albuquerque      Phoenix
    Boise         {Salt Lake City} Vancouver        Whitehorse
    Spokane        Seattle         Salem            Reno
    {Las Vegas}   {Los Angeles}   {San Diego}      {San Francisco}
    Anchorage      Fairbanks       Ketchikan        Honolulu
  } eu {
    {Unknown Timezone #0}
    Reykjavik  Dublin       Cork       London
    Cardiff    Edinburgh    Belfast    Liverpool
    Lisbon     Porto        Valletta   Madrid
    Barcelona  Valencia    {La Coruna} Seville
    Paris      Brest        Lyons      Bordeaux
    Marseilles Brussels     Bastogne   Amsterdam
    Rotterdam  Luxembourg   Berlin     Hamburg
    Essen      Frankfurt    Munich     Bern
    Geneve     Vaduz        Wien       Innsbruck
    Rome       Genova       Venezia    Palermo
    Sassari    Oslo         Bergen     Trondheim
    Copenhagen Odense       Stockholm  Gothenburg
    Helsinki   Turku        Mikkeli    Warszawa
    Gdansk     Poznan       Wroclaw    Krakow
    Praha      Bratislava   Kosice     Budapest
    Bucuresti  Ljubljana    Zagreb     Sarajevo
    Beograd    Skopje       Tirane     Sofiya
    Athinai    Thessaloniki Iraklion   Ankara
    Istanbul   Izmir        Konya      Adana
    Jerusalem  Pretoria    {Cape Town} Durban
    Wellington Auckland     Dunedin    Sydney
    Melbourne  Adelaide     Perth      Brisbane
  }}
  ranks {S A+ A A- B+ B B- C+ C C- D+ D D- F+ F F-}
  difficulties {
    1 {Easy {Normal 1} {Normal 2} Hard}
    2 {}
    3 {}
    4 {Normal Hard Nightmare}
  }
  titles {1 {
    {Trigger of Sol} {Gun Master}      Gladiator    Bishop
     Queen            Berserker        Death       {Solar Boy}
    {Dark Boy}       {Solar Merchant} {Running Boy} King
     Rook             Knight           Pawn
  } 2 {
    {Sword Master} {Spear Master} {Hammer Master} {Fist Master}
    {Gun Master}    Adept         {Day Walker}    {Adventurer}
     Agent          Collector     {Dark Hunter}   {Grand Master}
  } 3 {
     Adept        Gladiator {SP Agent}  Champion
    {Dark Hunter} Alchemist  Collector {Doll Master}
     Storyteller  Grandmaster
  } 4 {
    {Dark Knight}  {Sol Gunner}     {Sword Master} {Gun Master}
     Guardian      {Treasure Hunter} Collector      Huntmaster
    {Shooting Star} Gladiator       {Special Agent} Wanderer
     Adventurer    {Grand Master}
  }}
  sides {Red Black Grey}
  styles {Sword Spear Hammer Gun Fists {No Style}}
  endings {Otenko Everybody Nobody Sabata}
  swords {Vanargand Jormungandr Hel}
  guns {Knight Dragoon Bomber Witch Ninja}
  terrennials {Toasty Nero Ursula Ezra Alexander Tove {War Rock}}
  climates {
    {Balmy Sub-Tropical} {Arid Desert} {Tropical Rainforest}
    {Humind-Continental} {Frigid Arctic}
  }
}}

# bok::@ is a shortcut to lookup a string in bok::strings.  Arguments starting
# with "#" are used as list indices, other arguments are used as dict keys.
proc bok::@ {locale args} {
  variable strings
  set current [dict get $strings $locale]
  foreach arg $args {
    set current [expr {[string index $arg 0] == "#" ?
      [lindex $current [string range $arg 1 end]] : [dict get $current $arg]
    }]
  }
  return $current
}

# The character table is stored as a dictionary of lists indexed by the table's
# number formatted as "0x%X".  {0x0 0x1F 0x80 0x81 0x81}
set bok::ctable {0x0 {
  �   �  � � � � � � � � � � � � � �  � � � � � � � � � � �  � �  � � �
  { } ! \" # ÷ % & ' ( ) * + , - . /  0 1 2 3 4 5 6 7 8 9 :  ; <  = > ?
  @   A  B C D E F G H I J K L M N O  P Q R S T U V W X Y Z  [ ×  ] ^ _
  `   a  b c d e f g h i j k l m n o  p q r s t u v w x y z \{ | \} ¯ ⋅
} 0x1F {
  � Ä � Ç É Ñ Ö Ü á à â ä � å ç é  è ê ë í ì î ï ñ ó ò ô ö � ú ù û
  ü � ° � � � � � ß � � � � � � �  � � � � � � � � � � � � � � � �
  � ¿ ¡ � � � � � « » � � À � � Œ  œ � � � � � � � ý ÿ Ý � � � � �
  � � � � � � Â Ê Á Ë È Í Î Ï Ì Ó  Ô � Ò Ú Û Ù � � � � � � � � � �
} 0x80 {
  �  あ い う え お か き  く け こ さ し す せ そ
  よ た ち つ て と な に  ぬ ね の は ひ ふ へ ほ
  下 ま み む め も や ゆ  よ ら り る れ ろ わ を
  左 ん ぁ ぃ ぅ ぇ ぉ っ  ゃ ゅ ょ が ぎ ぐ げ ご
  右 ざ じ ず ぜ ぞ だ ぢ  づ で ど ば び ぶ べ ぼ
  東 ぱ ぴ ぷ ぺ ぽ ｡  ､   ~  ー …  �  �  �  �  �
  西 ア イ ウ エ オ カ キ  ク ケ コ サ シ ス セ ソ
  南 タ チ ツ テ ト ナ ニ  ヌ ネ ノ ハ ヒ フ ヘ ホ
  北 マ ミ ム メ モ ラ リ  ル レ ロ ヤ ユ ヨ ワ ヲ
  大 ン ァ ィ ゥ ェ ォ ッ  ャ ュ ョ ガ ギ グ ゲ ゴ
  中 ザ ジ ズ ゼ ゾ ダ ヂ  ヅ デ ド バ ビ ブ ベ ボ
  小 パ ピ プ ペ ポ ・ ：  ； 「 」 ＋ ×  ℃  ℉  �
  �  ↑  ↓  →  ←  ★  ♥  ♪   ヴ Ⅰ  Ⅱ  Ⅲ  �  �  �  �
  風 白 黒 赤 青 黄 緑 金  銀 紫 � 火 炎 災 水 氷
  永 太 陽 年 月 日 時 分  秒 春 夏 秋 冬 之 ヶ 々
  ＝ 丈 片 己 凶 歯 �  �   �  �  �  �  �  �  �  �
} 0x180 {
  �  あ い う え お か き  く け こ さ し す せ そ
  よ た ち つ て と な に  ぬ ね の は ひ ふ へ ほ
  下 ま み む め も や ゆ  よ ら り る れ ろ わ を
  左 ん ぁ ぃ ぅ ぇ ぉ っ  ゃ ゅ ょ が ぎ ぐ げ ご
  右 ざ じ ず ぜ ぞ だ ぢ  づ で ど ば び ぶ べ ぼ
  東 ぱ ぴ ぷ ぺ ぽ ｡  ､   ~  ー …  �  �  �  �  �
  西 ア イ ウ エ オ カ キ  ク ケ コ サ シ ス セ ソ
  南 タ チ ツ テ ト ナ ニ  ヌ ネ ノ ハ ヒ フ ヘ ホ
  北 マ ミ ム メ モ ラ リ  ル レ ロ ヤ ユ ヨ ワ ヲ
  大 ン ァ ィ ゥ ェ ォ ッ  ャ ュ ョ ガ ギ グ ゲ ゴ
  中 ザ ジ ズ ゼ ゾ ダ ヂ  ヅ デ ド バ ビ ブ ベ ボ
  小 パ ピ プ ペ ポ ・ ：  ； 「 」 ＋ ×  ℃  ℉  �
  �  ↑  ↓  →  ←  ★  ♥  ♪   ヴ Ⅰ  Ⅱ  Ⅲ  �  �  �  �
  風 白 黒 赤 青 黄 緑 金  銀 紫 �  火 炎 災 水 氷
  永 太 陽 年 月 日 時 分  秒 春 夏 秋 { } 之 ヶ 々
  ＝ 丈 片 己 凶 歯 �  �   �  �  �  �  �  �  �  �
} 0x81 {
  �  地 均 坂 塔 境 塊 填  場 増 堀 堤 壊 塚 域 城
  �  現 理 球 環 �  �  �   �  切 �  功 攻 項 崎 靖
  端 化 代 付 何 仕 任 仗  仲 伯 件 作 伝 休 体 仮
  住 佐 他 使 便 信 倍 借  価 値 低 侮 個 保 係 供
  侵 依 偉 備 偽 似 俊 傷  像 優 候 修 例 側 倒 働
  健 併 佳 倫 停 傲 儀 �   �  �  �  �  �  �  �  �
  衝 行 往 彼 役 徐 復 後  待 得 徳 術 街 御 徴 徹
  衛 打 払 押 択 技 抜 投  抗 持 担 指 捨 排 抵 挑
  推 提 携 授 接 掘 操 揮  捕 探 換 振 掛 援 拠 損
  拡 把 握 掃 撤 �  �  �   �  �  �  �  �  �  �  �
  状 牧 物 特 犠 牲 独 狙  猛 狂 狩 猫 狐 狼 獲 猟
  獄 性 快 悦 怪 悟 怖 情  慎 慢 燐 憶 �  �  �  �
  � 粒 料 粗 精 �  �  �    �  灯 灼 焼 煙 燥 燃 爆
  燼 札 材 林 杯 村 析 相  枚 板 松 根 格 槍 横 株
  様 棺 桶 桿 植 橋 構 機  械 �  欄 樹 樋 椛 椿 模
  根 標 �  �  �  和 利 科  称 程 種 移 秘 積 稼 稲
} 0x82 {
  �  礼 祈 社 祝 神 視 福  �  �  �  �  初 裕 複 被
  捕 紅 紀 約 紋 紙 細 組  統 終 純 練 級 緒 経 絵
  給 絃 納 紹 絡 結 続 継  絶 編 縁 博 織 総 縦 綾
  締 績 網 縮 絆 幻 郷 �   �  次 冷 凍 �  �  議 論
  訳 計 討 記 許 訓 詳 説  話 証 読 設 語 談 試 調
  誤 課 誠 誘 護 認 謙 誕  識 謝 泡 汚 浪 液 涙 汰
  沙 江 況 沢 泊 河 注 洋  泣 治 活 浴 浩 池 波 洗
  流 法 決 油 消 温 浮 海  �  �  �  �  �  �  �  �
}}

# bok::decstr converts a list of 8-bit integers to a string, mapping to
# bok::ctable.  If $tab is specified then the decoding only uses table $tab,
# essentially assuming that each 8-bit integer is preceded by $tab.
proc bok::decstr {src {tab ""}} {
  variable ctable
  if {$tab != ""} {
    set tab [format 0x%X $tab]
    if {![dict exists $ctable $tab]} {
      return -code error "invalid table $tab"
    }
  }
  while {[lindex $src end] == 0} {
    set src [lreplace $src end end]
  }
  set r ""
  set state ""
  foreach c $src {
    if {$state == ""} {
      if {$tab == ""} {
        set state [format 0x%X $c]
        if {[dict exists $ctable $state]} {continue}
        set state [format 0x%X [expr {$c >> 8}]]
      } else {
        set state $tab
      }
    }
    if {[dict exists $ctable $state]} {
      append r [lindex [dict get $ctable $state] [expr {$c & 0xFF}]]
    } else {
      append r �
    }
    set state ""
  }
  return $r
}

# bok::encstr converts a string to a list of 8-bit integers, mapping from
# bok::ctable.  If $tab is specified the encoder only uses the $tab table,
# essentially omitting the table-change characters.
proc bok::encstr {src {tab ""}} {
  variable ctable
  if {$tab != ""} {
    set tab [format 0x%X $tab]
    if {![dict exists $ctable $tab]} {
      return -code error "invalid table $tab"
    }
  }
  set k 0
  set r {}
  foreach c [split $src {}] {
    if {$c == "�"} {break}
    if {$tab != ""} {
      set k 0
      set t [lsearch -exact [dict get $ctable $tab] $c]
    } else {
      foreach {k s} $ctable {
        if {[set t [lsearch -exact $s $c]] >= 0} {break}
      }
    }
    if {$t < 0} {break}
    if {$k != "0"} {lappend r $k}
    lappend r $t
  }
  return $r
}

set bok::b64table {jp {
    あ い う え お か き く  け こ さ し す せ そ た
    ち つ て と な に ぬ ね  の は ひ ふ へ ほ ま み
    む め も や ゆ よ ら り  る れ ろ わ を が ぎ ぐ
    げ ご ざ じ ず ぜ ぞ だ  ぢ づ で ど ば び ぶ べ  ぼ
} en {
    B C D F G H J K  L M N P Q R S T  V W X Y Z
    b c d f g h j k  l m n p q r s t  v w x y z
    0 1 2 3 4 5 6 7  8 9 ? ! @ # = ^  > / - _ + : .
} lk {
    A B C D E F G H  I J K L M N O P  Q R S T U V W X  Y Z
    a b c d e f g h  i j k l m n o p  q r s t u v w x  y z
    1 2 3 4 5 6 7 8  9 0 ? = .
}}

# bok::decb64 decodes the base64 string $pass using the above tables into a list
# of 6-bit integers, ignoring any [:space:] characters.  If the $game is 4 the
# "jp" & "lk" tables are used, otherwise the "jp" & "en" tables are used.
proc bok::decb64 {pass {game 1} {lang ""}} {
  variable b64table
  set tables [list [dict get $b64table jp] \
      [dict get $b64table [expr {$game < 4 ? "en" : "lk"}]]]
  return [lmap c [split $pass {}] {
    foreach table $tables {
      if {[set j [lsearch -exact $table $c]] >= 0} {break}
    }
    if {[string is space $c] || $j >= 64} {continue}
    if {$j < 0} {return -code error "Invalid character: $c"}
    set j
  }]
}

# bok::encb64 encodes a base64 string from list, which is list of 6-bit integers
# using the appropriate alphabet for the $game / $lang pair.  $lang is set based
# on the region if it is set to an empty string.
proc bok::encb64 {list {game 1} {lang ""}} {
  variable b64table
  if {$lang == ""} {
    set lang [getlang $list $game 6]
  }
  set table [dict get $b64table [expr {
    $lang == "jp" ? "jp" : $game < 3 ? "en" : "lk"}]]
  return [join [lmap c $list {lindex $table $c}] {}]
}

# bok::spaceout inserts spaces every few characters for readability.  It uses
# the common amount for a particular $game and $lang combination.  5 if $game
# is 4, 6 if $lang is "jp", and finally 8 if $lang is "en".  Otherwise the
# number guessed based on the length of $str.
proc bok::spaceout {str {game ""} {lang ""}} {
  if {$game == "" && $lang == ""} {
    variable passwordlengths
    set k [lindex [dict keys [dict filter $passwordlengths value \
        [string length $str]]] 0]
  } else {
    set k $lang$game
  }
  switch -exact -- $k {
  4 - en4 - jp4 {set n 5}
  jp - jp1 - jp2 - jp3 {set n 6}
  en - en1 - en2 - en3 {set n 8}
  default {return $str}
  }
  for {set i $n} {$i < [string length $str]} {incr i; incr i $n} {
    set str "[string range $str 0 $i-1] [string range $str $i end]"
  }
  return $str
}

# bok::getint gets an integer of width $count at bit-offset $offset from the
# list $list of $width wide integers, interpreting it as a two's complement
# signed integer if $sign is true.  The bits are read and written LSb first.
proc bok::getint {list offset count {sign 0} {width 6}} {
  set count [expr {min($count, [llength $list] * $width - $offset)}]
  for {set v 0; set i 0} {$i < $count} {incr i; incr offset} {
    set j [expr {$offset / $width}]
    set k [expr {$offset % $width}]
    set v [expr {$v | ([lindex $list $j] >> $k & 1) << $i}]
  }
  if {$sign && $count > 0 && ($v >> $count - 1 & 1)} {
    set v [expr {-(~$v + 1 & (1 << $count) - 1)}]
  }
  return $v
}

# bok::putint puts the integer $v of width $count into list $listvar at
# bit-offset $offset.  $listvar is a list of $width wide integers.  Zeros are
# added to the list as needed.  $v is clamped to [0,2^$count) for unsigned
# integers ($sign is false), and [-2^($count-1),2^($count-1)) for signed
# integers ($sign is true). Signed numbers are converted to two's-complement
# formatted $width wide unsigned integers before writing.  The bits are written
# and read LSb first.
proc bok::putint {listvar v offset count {sign 0} {width 6}} {
  if {$listvar != ""} {upvar $listvar list} {set list {}}
  set m [expr {(1 << $count - ($sign && $count > 0 ? 1 : 0)) - 1}]
  set v [expr {$sign ? min(max($v, -($m + 1)), $m) + $m + 1 ^ $m + 1 :
      min(max($v, 0), $m)}]
  for {set i 0} {$i < $count} {incr i; incr offset} {
    set j [expr {$offset / $width}]
    set k [expr {$offset % $width}]
    while {[llength $list] <= $j} {lappend list 0}
    lset list $j [expr {
        [lindex $list $j] & ~(1 << $k) | ($v >> $i & 1) << $k}]
  }
  return $list
}

# bok::getstr gets the string converted from $count octets from the $list of
# $width wide integers starting at bit-offset $offset.  $table is passed to
# bok::decstr along with the octets.
proc bok::getstr {list offset count {table ""} {width 6}} {
  if {$table == "-"} {set table ""}
  if {$offset + $count * 8 > [llength $list] * $width} {
    set count [expr {[llength $list] * $width - $offset >> 3}]
  }
  for {set r {}; set i 0} {$i < $count} {incr i} {
    lappend r [getint $list [expr {$offset + $i * 8}] 8 0 $width]
  }
  return [decstr $r $table]
}

# bok::putstr puts $count octets from the encoded $str into $listvar at
# bit-offset $offset.  $listvar is a list of $width wide integers and $table
# is passed to bok::encstr along with $str.
proc bok::putstr {listvar str offset count {table ""} {width 6}} {
  if {$listvar != ""} {upvar $listvar list} {set list {}}
  if {$table == "-"} {set table ""}
  set l [encstr $str $table]
  for {set i 0} {$i < $count} {incr i} {
    set c [expr {$i < [llength $l] ? [lindex $l $i] : 0}]
    putint list $c [expr {$offset + $i * 8}] 8 0 $width
  }
  return $list
}

# bok::bitmaps is a dictionary of bitmaps with the keys being the password
# language and the game number: {jp1 jp2 jp3 jp4 en1 en2 en3 en4}
# each bitmap is a dictionary with list values: {type offset count modifier}
# The type is either "int" or "str".  The offset is the bit that the value
# starts at.  The count how long the value is in either bits (for type "int")
# or octets (for type "str").  The modifier for type "int" is a boolean meaning
# unsigned for false values or signed for true values; for type "str" it is the
# character table to use with "-" indicating the default ("").  The list items
# after the type can be used directly as arguments to bok::getint/bok::putint
# for type "int", or bok::getstr/bok::putstr for type "str".
set bok::bitmaps {
  jp1 {
    region       {int   0 3 0}  checksum    {int   3 16 0}
    offset       {int  19 2 0}  sol/4       {int  21  9 0}
    timezone     {int  30 6 0}  hours       {int  36  6 0}
    minutes      {int  42 6 0}  difficulty  {int  48  3 0}
    dungeons     {int  51 5 0}  clears      {int  56  3 0}
    continues    {int  59 4 0}  caught      {int  63  5 0}
    kills        {int  68 7 0}  rank        {int  75  4 0}
    title        {int  79 5 0}  name        {str  84  5 0x180}
    link-battles {int 124 6 0}  link-trades {int 130  6 0}
    loan         {int 136 8 0}  padding     {int 144  0 0}
  } jp2 {
    checksum      {int   0 16 0}  header-padding {int  16  2 0}
    region        {int  18  3 0}  offset         {int  21  3 0}
    timezone      {int  24  6 0}  side           {int  30  3 0}
    style         {int  33  4 0}  kills/8        {int  37  8 0}
    forges        {int  45  7 0}  link-battles   {int  52  6 0}
    link-shopping {int  58  6 0}  sol/4096       {int  64 10 1}
    loan/256      {int  74  8 1}  hours          {int  82  6 0}
    titles        {int  88 16 0}  name           {str 104  5 0x180}
    padding       {int 144  0 0}
  } jp3 {
    checksum   {int   0 16 0}  header-padding {int  16  2 0}
    region     {int  18  3 0}  offset         {int  21  3 0}
    timezone   {int  24  6 0}  kills/8        {int  30  8 0}
    forges     {int  38  7 0}  races          {int  45  9 0}
    link-races {int  54  7 0}  cross-linked   {int  61  1 0}
    endings    {int  62  6 0}  sol/4096       {int  68 10 0}
    loan/256   {int  78  8 0}  hours          {int  86  6 0}
    titles     {int  92 12 0}  name           {str 104  5 0x180}
    padding    {int 144  0 0}
  } jp4 {
    checksum   {int   0 16 0}  header-padding {int  16  2 0}
    region     {int  18  3 0}  offset         {int  21  3 0}
    titles     {int  24 14 0}  difficulty     {int  38  2 0}
    hours      {int  40  7 0}  sol/4096       {int  47 15 0}
    sword      {int  62  3 0}  gun            {int  65  3 0}
    terrennial {int  68  3 0}  climate        {int  71  3 0}
    name-dark  {str  74 10 -}  name-solar     {str 154 10 -}
    padding    {int 234  6 0}
  } en1 {
    region       {int   0 3 0}  checksum    {int   3 16 0}
    offset       {int  19 2 0}  sol/4       {int  21  9 0}
    timezone     {int  30 7 0}  hours       {int  37  6 0}
    minutes      {int  43 6 0}  difficulty  {int  49  3 0}
    dungeons     {int  52 5 0}  clears      {int  57  3 0}
    continues    {int  60 4 0}  caught      {int  64  5 0}
    kills        {int  69 7 0}  rank        {int  76  4 0}
    title        {int  80 5 0}  name        {str  85  9 0}
    link-battles {int 157 6 0}  link-trades {int 163  6 0}
    loan         {int 169 8 0}  padding     {int 177 15 0}
  } en2 {
    checksum      {int   0 16 0}  header-padding {int  16  2 0}
    region        {int  18  3 0}  offset         {int  21  3 0}
    timezone      {int  24  7 0}  side           {int  31  3 0}
    style         {int  34  4 0}  kills/8        {int  38  8 0}
    forges        {int  46  7 0}  link-battles   {int  53  6 0}
    link-shopping {int  59  6 0}  sol/4096       {int  65 10 1}
    loan/256      {int  75  8 1}  hours          {int  83  6 0}
    titles        {int  89 16 0}  name           {str 105  9 0}
    padding       {int 177 15 0}
  } en3 {
    checksum   {int   0 16 0}  header-padding {int  16  2 0}
    region     {int  18  3 0}  offset         {int  21  3 0}
    timezone   {int  24  7 0}  kills/8        {int  31  8 0}
    forges     {int  39  7 0}  races          {int  46  9 0}
    link-races {int  55  7 0}  cross-linked   {int  62  1 0}
    endings    {int  63  6 0}  sol/4096       {int  69 10 0}
    loan/256   {int  79  8 0}  hours          {int  87  6 0}
    titles     {int  93 12 0}  name           {str 105  9 0}
    padding    {int 177 15 0}
  } en4 {
    checksum   {int   0 16 0}  header-padding {int  16  2 0}
    region     {int  18  3 0}  offset         {int  21  3 0}
    titles     {int  24 14 0}  difficulty     {int  38  2 0}
    hours      {int  40  7 0}  sol/4096       {int  47 15 0}
    sword      {int  62  3 0}  gun            {int  65  3 0}
    terrennial {int  68  3 0}  climate        {int  71  3 0}
    name-dark  {str  74 10 -}  name-solar     {str 154 10 -}
    padding    {int 234  6 0}
  }
}

# bok::getdict returns a dictionary created by reading values from the $list of
# $width wide integers for each key in $args, or every key if $args is empty,
# as specified in bok::bitmaps for the $game / $lang pair.
proc bok::getdict {list game lang width args} {
  variable bitmaps
  if {[llength $args] == 0} {
    set args [dict keys [dict get $bitmaps $lang$game]]
  }
  set dict {}
  foreach key $args {
    if {![dict exists $bitmaps $lang$game $key]} {continue}
    lassign [dict get $bitmaps $lang$game $key] type offset count mod
    switch -exact -- $type str {
      dict set dict $key [getstr $list $offset $count $mod $width]
    } int {
      dict set dict $key [getint $list $offset $count $mod $width]
    }
  }
  return $dict
}

# bok::getkeys returns the list of values from calling bok::getdict.
proc bok::getkeys {list game lang width args} {
  return [dict values [getdict $list $game $lang $width {*}$args]]
}

# bok::getkey returns only the value from calling bok::getdict with $key.
proc bok::getkey {list game lang width key} {
  return [lindex [getkeys $list $game $lang $width $key] 0]
}

# bok::putdict inserts the values from each key/value pair into the $listvar of
# $width wide integers as specified with the corresponding key in bok::bitmaps
# for the $game / $lang pair.
proc bok::putdict {listvar game lang width args} {
  variable bitmaps
  if {$listvar != ""} {upvar $listvar list} {set list {}}
  if {[llength $args] == 1} {set args [concat {*}$args]}
  if {[llength $args] % 2 != 0} {
    set usage {putdict listvar game lang width ?dict | key value ...?}
    return -code error "wrong # args: should be \"$usage\""
  }
  foreach {key value} $args {
    if {![dict exists $bitmaps $lang$game $key]} {continue}
    lassign [dict get $bitmaps $lang$game $key] type offset count mod
    switch -exact -- $type str {
      putstr list $value $offset $count $mod $width
    } int {
      putint list $value $offset $count $mod $width
    }
  }
  return $list
}

# bok::modkeys executes the last argument for each key given in $args, with 
# the key, value, and count values set to variables specified in $varnames.
# If $varnames only contains one member then it is used for the storing the
# value.  The key and count values are from the bitmap specified in bok::bitmaps
# for the $game / $lang pair, and the value itself is extracted from the list of
# $width wide integers in $listvar.  The result of the executed body is then
# put back into $listvar in place of what was extracted.  Non-existent or out
# of range keys are ignored, continues skip overwriting the value, and breaks
# exit early.  This proc returns the final value of $listvar.
proc bok::modkeys {listvar game lang width varnames args} {
  if {$listvar == "" || $varnames == "" || [llength $args] < 1} {
    set usage {modify listvar game lang width varnames ?key ...? body}
    return -code error "wrong # args: should be \"$usage\""
  }
  variable bitmaps
  upvar $listvar list
  if {[llength $varnames] > 1} {
    lassign $varnames keyname varname countvar
    if {$keyname != ""} {upvar $keyname key}
    if {$varname != ""} {upvar $varname var}
    if {$countvar != ""} {upvar $countvar count}
  } elseif {$varnames != ""} {
    upvar $varnames var
  }
  set body [lindex $args end]
  if {[llength $args] == 1} {
    set args [dict keys [dict get $bitmaps $lang$game]]
  } else {
    set args [lrange $args 0 end-1]
  }
  set max [expr {[llength $list] * $width}]
  foreach key $args {
    if {![dict exists $bitmaps $lang$game $key]} {continue}
    lassign [dict get $bitmaps $lang$game $key] type offset count mod
    if {$offset >= $max} {continue}
    switch -exact -- $type int {
      set var [getint $list $offset $count $mod $width]
      putint list [uplevel 1 $body] $offset $count $mod $width
    } str {
      set var [getstr $list $offset $count $mod $width]
      putstr list [uplevel 1 $body] $offset $count $mod $width
    }
  }
  return $list
}

# bok::getregion reads the region bits from the $list of $width wide integers
# and returns a short region code, or an error.
proc bok::getregion {list {game 1} {width 6}} {
  switch -exact -- [set t [getkey $list $game jp $width region]] {
  1 {return jp} 2 {return na} 3 {return eu}
  }
  return -code error "Invalid region #$t"
}

# bok::getlang reads the region bits from the $list of $width wide integers and
# returns a short language code or an error.
proc bok::getlang {list {game 1} {width 6}} {
  switch -exact -- [set t [getkey $list $game jp $width region]] {
  1 {return jp} 2 - 3 {return en}
  }
  return -code error "Invalid region #$t"
}

# bok::whichgame searches $dict for keys specific to a particular game, and
# returns the game number of the first one found.
proc bok::whichgame {dict} {
  foreach {key game} {
    dungeons 1 clears 1 caught 1 rank 1 title 1 link-trades 1
    rank-name 1 title-name 1
    side 2 style 2 link-shopping 2
    side-name 2 style-name 2
    races 3 link-races 3 cross-linked 3 endings 3
    sword 4 gun 4 terrennial 4 climate 4 name-dark 4 name-solar 4
    sword-name 4 gun-name 4 terrennial-name 4 climate-name 4
  } {
    if {[dict exists $dict $key]} {return $game}
  }
  return -code error "Unknown game"
}

# bok::checksum calculates the CRC-16 for the $list of 6-bit integers with an
# initializer of 0xFFFF, the $game appropriate polynomial from bok::sumconsts,
# and the output inverted, and starting at the offset from bok::sumoffsets.
set bok::sumoffsets {4 3 3 3}
set bok::sumconsts {0x1021 0x1021 0x8005 0x180D}
proc bok::checksum {list {game 1}} {
  variable sumoffsets
  variable sumconsts
  for {
    set c [lindex $sumconsts $game-1]
    set v 0xFFFF
    set i [lindex $sumoffsets $game-1]
  } {$i < [llength $list]} {incr i} {
    set v [expr {$v ^ [lindex $list $i] << 8}]
    for {set j 0} {$j < 8} {incr j} {
      set v [expr {($v << 1 ^ (($v & 0x8000) ? $c : 0)) & 0xFFFF}]
    }
  }
  return [expr {~$v & 0xFFFF}]
}

# bok::xor encodes/decodes the password bits from the $list of 6-bit integers
# using bok::xorconst and the $game appropriate initalizer from bok::xorconsts. # $m is the initialized value, $t is initialized to ~0, and the offset is
# read from the $list.  For each iteration $m is multiplied by bok::xorconst
# then incremented by 1, and $t is xored by the result.  After $offset plus 4
# iterations, starting at index 4 until the end of the $list, each $list item
# is xored by the last 6 bits of $t before the next iteration.
# The encoded list is returned.
# Using the full sized constants can casue unnecessary integer promotion,
# and only the last 6 bits are used, so smaller constants are used here.
#set bok::xorconsts {0x8C159 0xB8E6E 0x8C159 0x5BB15}
set bok::xorconsts {0x19 0x2E 0x19 0x15}
#set bok::xorconst 0x6262C05D
set bok::xorconst 0x1D
proc bok::xor {list {game 1}} {
  variable xorconsts
  variable xorconst
  for {
    set m [lindex $xorconsts $game-1]
    set t 0x3F
    set i -[getkey $list $game jp 6 offset]
  } {$i < [llength $list]} {incr i} {
    if {$i >= 4} {
      lset list $i [expr {[lindex $list $i] ^ $t}]
    }
    set m [expr {$xorconst * $m + 1 & 0x3F}]
    set t [expr {$t ^ $m}]
  }
  return $list
}

set bok::passwordlengths {
  jp1 24 jp2 24 jp3 24 jp4 40 en1 32 en2 32 en3 32 en4 40
}
# bok::decpass decodes the base64 string $password into a dictionary.
proc bok::decpass {password {game 1} {lang ""}} {
  variable passwordlengths
  set l [decb64 $password $game]
  if {$lang == ""} {
    set lang [getlang $l $game]
  }
  set passlen [dict get $passwordlengths $lang$game]
  if {[llength $l] != $passlen} {
    return -code error "Invalid length: [llength $l] != $passlen"
  }
  set sum [checksum $l $game]
  set l [xor $l $game]
  set dict [getdict $l $game $lang 6]
  if {[dict get $dict checksum] != $sum} {
    dict append dict checksum "!=$sum"
  }
  return $dict
}

# bok::encpass encodes $dict into a base64 password.
proc bok::encpass {dict {lang ""}} {
  if {$lang == ""} {
    set lang [lindex {jp jp en en} [dict get $dict region]]
  }
  set game [whichgame $dict]
  set l [xor [putdict {} $game $lang 6 $dict] $game]
  putdict l $game $lang 6 checksum [checksum $l $game]
  return [encb64 $l $game $lang]
}

# bok::normalizelist sets {*}$skey in $dictvar based on the value of {*}$vkey
# based on the strings in $list, or vice versa.  $name is used for error
# messages, and defaults to the last element of $vkey.  If $robust is set then
# no errors are returned for invalid values, or known invalid strings.
proc bok::normalizelist {dictvar list vkey skey {name ""} {robust 0}} {
  upvar $dictvar dict
  if {$name == ""} {
    set name [lindex $vkey end]
  }
  set inval "Invalid $name #"
  set unknown "Unknown $name #"
  if {[dict exists $dict {*}$vkey]} {
    set v [dict get $dict {*}$vkey]
    set s "$unknown$v"
    if {$v >= 0 && $v < [llength $list]} {
      set s [lindex $list $v]
    }
    if {!$robust && $s == "$unknown$v"} {
      return -code error $s
    }
    dict set dict {*}$skey $s
  } elseif {[dict exists $dict {*}$skey]} {
    set s [dict get $dict {*}$skey]
    set v [lsearch -exact -nocase $list $s]
    if {$v < 0 && [string first $inval $s] == 0} {
      set v [string range $s [string length $inval] end]
      if {![string is integer $v]} {set v -1}
      if {!$robust} {
        return -code error $s
      }
    } elseif {$v < 0 && [string first $unknown $s] == 0} {
      set v [string range $s [string length $unknown] end]
      if {![string is integer $v]} {set v -1}
      if {!$robust} {
        return -code error $s
      }
    }
    if {$v < 0} {
      return -code error "Invalid [lindex $skey end]: $s"
    }
    dict set dict {*}$vkey $v
  }
}

# bok::normalizebits sets {*}$lkey in $dictvar based on the value of {*}$vkey
# based on the strings in $list, or vice versa.  $name is used for error
# messages, and defaults to the last element of $vkey.  If $robust is set then
# invalid values and items are ignored.
proc bok::normalizebits {dictvar list vkey lkey {name ""} {robust 0}} {
  upvar $dictvar dict
  if {$name == ""} {
    set name [lindex $vkey end]
  }
  if {[dict exists $dict {*}$vkey]} {
    dict set dict {*}$vkey [format 0x%0[expr {
      [llength $list] + 3 >> 2
    }]X [dict get $dict {*}$vkey]]
    set v [dict get $dict {*}$vkey]
    for {set i 0; set l {}} {$i < [llength $list]} {incr i} {
      if {$v & 1 << $i} {
        set v [expr {$v ^ 1 << $i}]
        lappend l [lindex $list $i]
      }
    }
    if {!$robust && $v != 0} {
      for {} {!($v & 1 << $i)} {incr i} {}
      return -code error "Unknown $name #$i"
    }
    dict set dict {*}$lkey $l
  } elseif {[dict exists $dict {*}$lkey]} {
    set v 0
    foreach item [dict get $dict {*}$lkey] {
      set i [lsearch -exact -nocase $list $item]
      if {!$robust && $i < 0} {
        return -code error "Unknown $name: $item"
      }
      set v [expr {$v | ($i >= 0 ? 1 << $i : 0)}]
    }
    dict set dict {*}$vkey $v
  }
}

# bok::normalize normalizes the provided dictionary using $locale strings
# such that if "sol/4096" or "timezone-name" exists then "sol" and "timezone"
# will contain the equivalent value in the result.  If $robust is set then no
# ignorable errors are returned.
proc bok::normalize {dict {locale en} {robust 0}} {
  # Identify game, language, and region
  set game [whichgame $dict]
  normalizelist dict [@ $locale regions] region region-name \
      [@ $locale labels region] $robust
  set region [expr {[dict exists $dict region] ? [dict get $dict region] : -1}]
  if {!$robust && ($region < 1 || $region > 3)} {
    return -code error "Unknown region"
  }
  set lang [lindex {?? jp en en ?? ?? ?? ??} $region]
  set region [lindex {?? jp na eu ?? ?? ?? ??} $region]
  # Normalize
  dict set dict game $game
  dict set dict game-name [@ $locale games $lang$game]
  if {![dict exists $dict header-padding]} {dict set dict header-padding 0}
  if {![dict exists $dict padding]} {dict set dict padding 0}
  if {![dict exists $dict offset]} {dict set dict offset 0}
  # Sol
  if {[dict exists $dict sol]} {
    dict set dict sol/4 [expr {int([dict get $dict sol] / 4)}]
    dict set dict sol/4096 [expr {int([dict get $dict sol] / 4096)}]
  } elseif {[dict exists $dict sol/4]} {
    dict set dict sol [expr {[dict get $dict sol/4] * 4}]
    dict set dict sol/4096 [expr {int([dict get $dict sol/4] / 1024)}]
  } elseif {[dict exists $dict sol/4096]} {
    dict set dict sol [expr {[dict get $dict sol/4096] * 4096}]
    dict set dict sol/4 [expr {[dict get $dict sol/4096] * 1024}]
  }
  # Kills
  if {[dict exists $dict kills]} {
    dict set dict kills/8 [expr {int([dict get $dict kills] / 8)}]
  } elseif {[dict exists $dict kills/8]} {
    dict set dict kills [expr {[dict get $dict kills/8] * 8}]
  }
  # Loan
  if {[dict exists $dict loan]} {
    dict set dict loan/256 [expr {int([dict get $dict loan] / 256)}]
  } elseif {[dict exists $dict loan/256]} {
    dict set dict loan [expr {[dict get $dict loan/256] * 256}]
  }
  # Timezone
  set l [@ $locale timezones]
  if {[dict exists $l $region]} {
    normalizelist dict [dict get $l $region] timezone timezone-name \
        [@ $locale labels timezone] $robust
  }
  # Boktai 1
  normalizelist dict [@ $locale ranks] rank rank-name [@ $locale labels rank] \
      $robust
  normalizelist dict [@ $locale titles 1] title title-name \
      [@ $locale labels title] $robust
  normalizelist dict [@ $locale difficulties $game] difficulty difficulty-name \
      [@ $locale labels difficulty] $robust
  # Boktai 2
  normalizelist dict [@ $locale sides] side side-name [@ $locale labels side] \
      $robust
  normalizelist dict [@ $locale styles] style style-name \
      [@ $locale labels style] $robust
  # Boktai 2, Boktai 3, and Lunar Knight's Titles
  normalizebits dict [@ $locale titles $game] titles title-list \
      [@ $locale labels titles] $robust
  # Boktai 3's Endings
  normalizebits dict [@ $locale endings] endings ending-list \
      [@ $locale labels endings] $robust
  # Lunar Knights favorites
  normalizelist dict [@ $locale swords] sword sword-name \
      [@ $locale labels sword] $robust
  normalizelist dict [@ $locale guns] gun gun-name [@ $locale labels gun] \
      $robust
  normalizelist dict [@ $locale terrennials] terrennial terrennial-name \
      [@ $locale labels terrennial] $robust
  normalizelist dict [@ $locale climates] climate climate-name \
      [@ $locale labels climate] $robust
  return $dict
}

namespace eval bok::ui {
  namespace export *
  namespace import [namespace parent]::*
}

# bok::ui::info returns a list of ui element types, values, and defaults
# depending on the values of $game, $region, and $locale.  Most values and
# defaults can be dictionaries keyed by region.  A key of "-" makes the next
# widget always start on a new row.
# key {int {min ?max?} ?defaults? ?step?}
# key {string {max ?maxbytes?} ?defaults? ?restrict?}
# key {list ?values? ?defaults?}
# key {boolean ?default?}
# key {bits values ?default?}
# - {}
proc bok::ui::info {game {region na} {locale en}} {
  set region [expr {max([region $region $locale num] - 1, 0)}]
  set timezone [expr {
    $region == 0 ? 5 : $region == 2 ? 21 : $region == 1 ? 14 : 0
  }]
  set timezonelists [dict map {key value} [@ $locale timezones] {
    lrange $value 1 end
  }]
  set regions [lrange [@ $locale regions] 1 end]
  switch -exact -- $game 1 {
    return [dict create \
        region-name [list list $regions $region] \
        offset {int {0 3}} \
        sol {int {0 0x7FC 4}} \
        timezone-name [list regionlist $timezonelists {jp 5 na 14 eu 21}] \
        - {} hours {int {0 63}} minutes {int {0 59} 1} \
        difficulty-name [list list [@ $locale difficulties 1] 2] \
        dungeons {int {0 29}} \
        clears {int {1 7}} continues {int {0 15}} \
        caught {int {0 31}} kills {int {0 127}} \
        rank-name [list list [@ $locale ranks]] \
        title-name [list list [@ $locale titles 1]] \
        name {string {jp 5 na 9 eu 9} {jp ジャンゴ na Django eu Django} 1} \
        link-battles {int {0 63}} link-trades {int {0 63}} \
        loan {int {0 255}} \
        padding hidden \
    ]
  } 2 {
    return [dict create \
        region-name [list list $regions $region] \
        offset {int {0 7}} \
        timezone-name [list regionlist $timezonelists {jp 5 na 14 eu 21}] \
        side-name [list list [@ $locale sides]] \
        style-name [list list [@ $locale styles]] \
        kills {int {0 0x7F8 8}} \
        forges {int {0 127}} \
        link-battles {int {0 63}} link-shopping {int {0 63}} \
        sol {int {0 0x1FF000 0x1000}} loan {int {0 0x7F00 0x100}} \
        hours {int {0 63}} \
        name {string {jp 5 na 9 eu 9} {jp ジャンゴ na Django eu Django} 1} \
        title-list [list bits [@ $locale titles 2] 0x40] \
        header-padding hidden padding hidden \
    ]
  } 3 {
    return [dict create \
        region-name [list list [lindex $regions 0] 0] \
        offset {int {0 7}} \
        timezone-name [list regionlist $timezonelists {jp 5 na 14 eu 21}] \
        kills {int {0 0x7F8 8}} forges {int {0 127}} races {int {0 511}} \
        link-races {int {0 127}} cross-linked {boolean} \
        sol {int {0 0x3FF000 0x1000}} loan {int {0 0xFF00 0x100}} \
        hours {int {0 63}} \
        name {string {jp 5 na 9 eu 9} {jp ジャンゴ na Django eu Django} 1} \
        ending-list [list bits [@ $locale endings] 1] \
        title-list [list bits [@ $locale titles 3]] \
        header-padding hidden padding hidden \
    ]
  } 4 {
    return [dict create \
        region-name [list list $regions $region] \
        offset {int {0 7}} \
        difficulty-name [list list [@ $locale difficulties 4]] \
        hours {int {0 127}} sol {int {0 0x7FFF000 0x1000}} \
        - {} \
        sword-name [list list [@ $locale swords]] \
        gun-name [list list [@ $locale guns]] \
        terrennial-name [list list [@ $locale terrennials]] \
        climate-name [list list [@ $locale climates]] \
        - {} \
        name-dark {string {jp {5 10} na 10 eu 10}
          {jp サバタ na Lucian eu Lucian}} \
        name-solar {string {jp {5 10} na 10 eu 10}
          {jp ジャンゴ na Aaron eu Aaron}} \
        title-list [list bits [@ $locale titles 4]] \
        header-padding hidden padding hidden \
    ]
  }
}

# bok::ui::region translates the $str to the region "number", two-letter "code",
# "name", or a list of all three depending on $type.  $locale selects which
# region list from bok::strings to use.
proc bok::ui::region {str {locale en} {type code}} {
  set dict {}
  switch -glob -nocase -- $str {
  jp - japan - japanese {dict set dict region 1}
  na - north?america - northamerica - america {dict set dict region 2}
  us - usa - united?states - unitedstates {dict set dict region 2}
  en - english {dict set dict region 2}
  eu - europe {dict set dict region 3}
  default {dict set dict region-name $str}
  }
  normalizelist dict [@ $locale regions] region region-name \
      [@ $locale labels region] 1
  set region [dict get $dict region]
  set str [dict get $dict region-name]
  set code [lindex {?? jp na eu ?? ?? ?? ??} $region]
  switch -exact -- $type {
  short - code {
    return $code
  } string - str - name {
    return $str
  } number - num {
    return $region
  } default {
    return [list $region $code $str]
  }}
}

# bok::ui::init initializes a grid of widgets in frame $window based on the
# values from bok::ui::info for region $region.  Valid options are -locale to
# set the locale to use, -columns for the number of columns of label/entry pairs
# to use, -bind for a list of bindings for matching elements, -style for a list
# of styles for matching elements, -fontsize for the base font size, and
# -validate to pass when creating widgets.
# -bind and -style keys can match either the widget type, the widget subtype,
# the widget path, or "all".  For example button $widget.password.cycleoffset
# matches the widget path, button, password.button, or "all".
proc bok::ui::init {window region game args} {
  set usage {init path {jp|na|eu} {1|2|3|4} ?-option value ...?}
  if {[llength $args] % 2 != 0} {
    return -code error "wrong # args: should be \"$usage\""
  }
  set options [dict create \
      -locale en -columns 2 -bind {} -style {} -fontsize 10 -validate all]
  foreach {opt arg} $args {
    switch -exact -- $opt -locale - -columns - -fontsize - -validate {
      dict set options $opt $arg
    } -bind - -style {
      dict lappend options $opt {*}$arg
    } default {
      return -code error "bad option: $opt"
    }
  }
  set columns [expr {[dict get $options -columns] * 2}]
  set locale [dict get $options -locale]
  set region [region $region $locale]
  if {$region == "??" || $game < 1 || $game > 4} {
    return -code error "wrong args: should be \"$usage\""
  }
  if {![winfo exists $window]} {
    ttk::frame $window
  }
  set row 0
  set col $columns
  grid [ttk::frame $window.password] \
      -row $row -column 0 -columnspan $columns -sticky nsew
  pack [ttk::button $window.password.encode -text ↱ -width 2 -takefocus 0 \
      -command [list [namespace current]::encode $window $game $locale]
  ] -padx 2p -side left
  pack [ttk::entry $window.password.entry \
      -font "mono [expr {int([dict get $options -fontsize]*1.2)}]"] \
      -expand 1 -side left -fill x
  pack [ttk::button $window.password.cycleoffset -text ↻ -width 2 -takefocus 0 \
      -command [list [namespace current]::cycleoffset $window $game]
  ] -padx 2p -side left
  pack [ttk::button $window.password.decode -text ↴ -width 2 -takefocus 0 \
      -command [list [namespace current]::decode $window $game $locale]
  ] -padx 2p -side left
  set elements [list \
      frame password $window.password \
      button password.button $window.password.encode \
      entry password.entry $window.password.entry \
      button password.button $window.password.cycleoffset \
      button password.button $window.password.decode
  ]
  set sublabellengths {}
  foreach {key list} [info $game $locale] {
    if {$list == ""} {
      set col $columns
      continue
    }
    if {[lindex $list 0] == "hidden"} {
      if {[llength $list] < 2} {set list {hidden 0}}
      set default [lindex $list 1]
      if {[llength $default] > 1} {set default [dict get $defalut $region]}
      set ::$window.$key $default
      continue
    }
    if {[lindex $list 0] == "bits"} {
      set col $columns
    }
    if {[incr col] >= $columns} {
      set col 0
      incr row
    }
    grid [ttk::label $window.$key\-label -anchor e \
        -text [@ $locale labels $key]:
    ] -row $row -column $col -sticky new
    if {[lindex $list 0] != "bits"} {
      bind $window.$key\-label <ButtonPress> [list focus $window.$key]
    }
    incr col
    lappend elements label $key.label $window.$key\-label
    switch -exact -- [lindex $list 0] string {
      lassign [lrange $list 1 end] l default type
      grid [ttk::entry $window.$key -validate [dict get $options -validate] \
          -validatecommand [list [namespace current]::validatestring \
              $window %W %V %s %P $l $default $locale $type] \
          -font "sans [dict get $options -fontsize]"
      ] -row $row -column $col -sticky new
      if {[llength $default] > 1} {
        set default [dict get $default $region]
      }
      $window.$key insert 0 $default
      lappend elements entry string $window.$key
    } int {
      lassign [lrange $list 1 end] l default
      if {$default == ""} {set default [lindex $l 0]}
      if {[llength $l] == 1} {lappend l $l}
      lappend l 1
      grid [ttk::spinbox $window.$key \
          -font "sans [dict get $options -fontsize]" \
          -from [lindex $l 0] -to [lindex $l 1] -increment [lindex $l 2] \
          -validate [dict get $options -validate] \
          -validatecommand [list \
              [namespace current]::validateint $window %W %V %s %P $default]
      ] -row $row -column $col -sticky new
      if {[llength $default] > 1} {set default [dict get $default $region]}
      $window.$key set $default
      lappend elements spinbox int $window.$key
    } boolean {
      set default [lindex $list 1]
      grid [ttk::checkbutton $window.$key] -row $row -column $col -sticky new
      $window.$key state !alternate
      if {[llength $default] > 1} {set default [dict get $default $region]}
      if {$default != "" && !!$default} {
        $window.$key state selected
      }
      bind $window.$key\-label <ButtonPress> [list $window.$key invoke]
      lappend elements checkbutton boolean $window.$key
    } regionlist - list {
      lassign [lrange $list 1 end] l default
      if {[lindex $list 0] == "regionlist"} {set l [dict get $l $region]}
      grid [ttk::combobox $window.$key -state readonly -values $l \
          -font "sans [dict get $options -fontsize]" \
      ] -row $row -column $col -sticky new
      if {[llength $default] > 1} {set default [dict get $default $region]}
      $window.$key current [expr {$default == "" ? 0 : $default}]
      lappend elements combobox [lindex $list 0] $window.$key
    } bits {
      lassign [lrange $list 1 end] l default
      if {[llength $default] > 1} {set default [dict get $default $region]}
      grid [ttk::frame $window.$key -borderwidth 1] \
        -column $col -row $row -columnspan [expr {$columns - $col}] -sticky new
      set i 0
      set x 0
      set y 0
      foreach k $l {
        lappend elements label bits.label $window.$key.l$i
        grid [ttk::label $window.$key.l$i -text $k: -anchor e \
          -font "sans [dict get $options -fontsize]"] \
            -column $x -row $y -sticky new
        bind $window.$key.l$i <ButtonPress> [list $window.$key.$i invoke]
        if {![dict exists $sublabellengths $x] ||
            [dict get $sublabellengths $x] < [string length $k:]} {
          dict set sublabellengths $x [string length $k:]
        }
        incr x
        grid [ttk::checkbutton $window.$key.$i] \
            -column $x -row $y -sticky new
        $window.$key.$i state !alternate
        if {$default != "" && ($default & 1 << $i) != 0} {
          $window.$key.$i state selected
        }
        if {[incr x] >= $columns * 2} {
          set x 0
          incr y
        }
        lappend elements checkbutton bits.checkbutton $window.$key.$i
        incr i
      }
      if {$x > 0} {
        set x 0
        incr y
      }
      grid $window.$key -rowspan $y
      set col $columns
      incr row [expr {$y - 1}]
      lappend elements frame bits $window.$key
    }
  }
  if {$col > 0} {
    incr row
  }
  grid [ttk::label $window.error -text ✓] -column 0 -row $row -sticky sew \
      -columnspan $columns
  for {set col 1} {$col < $columns} {incr col 2} {
    grid columnconfigure $window $col -weight 1
  }
  for {set col 0} {$col < $columns} {incr col} {
    grid columnconfigure $window $col -pad 4p
  }
  for {set y 0} {$y < $row} {incr y} {
    grid rowconfigure $window $y -pad 2p
  }
  bind $window.region-name <<ComboboxSelected>> \
      [list [namespace current]::updateregion $window $game $locale]
  foreach {type key w} $elements {
    switch -exact -- $key bits {
      dict for {col width} $sublabellengths {
        foreach label [grid slaves $w -column $col] {
          $label configure -width $width
        }
      }
    }
    foreach {k binding} [dict get $options -bind] {
      if {[string equal -nocase $k all] ||
          [string equal -nocase $k $type] ||
          [string equal -nocase $k $key]} {
        foreach {pat body} $binding {
          bind $w $pat $body
        }
      }
    }
    foreach {k style} [dict get $options -style] {
      if {[string equal -nocase $k all] ||
          [string equal -nocase $k $type] ||
          [string equal -nocase $k $key]} {
        $w configure -style $style
      }
    }
  }
  return $window
}

# bok::ui::updateregion updates any region dependant entries/lists in $window
# based on the value of $window.region-name and $locale.
proc bok::ui::updateregion {window game {locale en}} {
  set region [region [$window.region-name get] $locale]
  if {$region == "??"} {
    $window.timezone-name set [@ $locale timezones \#0]
    $window.timezone-name configure state disabled
    return
  }
  set lang [expr {$region == "jp" ? "jp" : "en"}]
  set pass [decb64 [$window.password.entry get] $game]
  modkeys pass $game $lang 6 {k v} region {
    expr {$region == "jp" ? 1 : $region == "na" ? 2 : 3}
  }
  foreach {key list} [info $game $region $locale] {
    switch -exact -- [lindex $list 0] regionlist {
      $window.$key configure -values [dict get [lindex $list 1] $region]
      if {[llength [lindex $list 2]] > 1} {
        $window.$key current [dict get [lindex $list 2] $region]
      } elseif {[lindex $list 2] != ""} {
        $window.$key current [lindex $list 2]
      } else {
        $window.$key current 0
      }
    } string {
      $window.$key validate
    }
  }
  if {[llength $pass] > 0} {
    $window.password.entry delete 0 end
    $window.password.entry insert 0 [spaceout [
      encb64 $pass $game $lang] $game $lang]
  }
}

# bok::ui::validatestring validates the string in entry $window.
# $root is the frame initialized by bok::ui::init, $window is the entry, $type
# is the event type, $old is the old value, $new is the new value, $max is
# {max ?maxbytes?} which limits the length of the string, default is either a
# default string or a dictionary of default strings where the region in $root
# is used as the key, $locale is the locale to use for the region name, and
# $restrict if true restricts the character table to either 0x180 if the region
# is Japan, or 0 otherwise.
proc bok::ui::validatestring {root window type old {new {}} {max 0} {default {}}
    {locale en} {restrict 0}} {
  set region [region [$root.region-name get] $locale]
  set tab [expr {
    $restrict == "" || !$restrict ? "" : $region == "jp" ? 0x180 : 0
  }]
  if {[llength $max] > 2} {set max [dict get $max $region]}
  if {[llength $max] == 1} {lappend max $max}
  lassign $max max bytemax
  switch -exact -- $type key {
    set list [encstr $new $tab]
    return [expr {
        ($max <= 0 || [string length $new] <= $max) &&
        ($bytemax <= 0 || [llength $list] <= $bytemax) &&
        [decstr $list $tab] == $new
    }]
  } default {
    if {$old != ""} {
      set list [encstr $new $tab]
      if {$bytemax > 0 && [llength $list] > $bytemax} {
        set list [lrange $list 0 $bytemax-1]
      }
      set new [decstr $list $tab]
      if {$max > 0 && [string length $new] > $max} {
        set new [string range $new 0 $max-1]
      }
      if {$new == $old} {return 1}
      $window delete 0 end
      if {$new != ""} {
        $window insert 0 $new
        return 0
      }
    }
    if {$default != ""} {
      if {[llength $default] > 1} {set default [dict get $default $region]}
      $window insert 0 $default
    }
    return 0
  }
}

# bok::ui::validateint validates the value of spinbox $window is an integer.
# $root is the frame initialized by bok::ui::init $window is the spinbox, $type
# is the event type, $old is the old value, $new is the new value, and $default
# is the default value.
proc bok::ui::validateint {root window type old {new {}} {default {}}} {
  switch -exact -- $type key {
    if {![string is integer $new]} {
      return 0
    } elseif {$new != "" && $new < [$window cget -from]} {
      $window set [expr {int([$window cget -from])}]
      return 0
    } elseif {$new != "" && $new > [$window cget -to]} {
      $window set [expr {int([$window cget -to])}]
      return 0
    }
    return 1
  } default {
    if {$old == "" || ![string is integer $old]} {
      if {![string equal $old $default]} {
        $window set $default
      }
      return 0
    }
    return 1
  }
}

# bok::ui::encode encodes the widget values into the password entry.
# $window is the window initialized with bok::ui::init, $game is the game that
# $window is for, and $locale is the locale of bok::strings to use.
proc bok::ui::encode {window game {locale en}} {
  if {$game < 1 || $game > 4} {
    return -code error \
        {wrong args: should be "encode path game ?locale?"}
  }
  set region [region [$window.region-name get] $locale]
  set info [info $game $region $locale]
  set dict {}
  foreach {key list} $info {
    switch -exact -- [lindex $list 0] {
    {} {continue}
    string - int - regionlist - list {
      dict set dict $key [$window.$key get]
    } boolean {
      dict set dict $key [expr {"selected" in [$window.$key state]}]
    } bits {
      dict set dict $key {}
      for {set i 0} {$i < [llength [lindex $list 1]]} {incr i} {
        if {"selected" in [$window.$key.$i state]} {
          dict lappend dict $key [lindex $list 1 $i]
        }
      }
    } hidden {
      dict set dict $key [set ::$window.$key]
    }}
  }
  $window.error configure -text ✓
  if {[catch {normalize $dict $locale} error]} {
    $window.error configure -text "⚠ $error"
  }
  set dict [normalize $dict $locale 1]
  if {[catch {set pass [encpass $dict]} error]} {
    $window.error configure -text "⚠ $error"
    return
  }
  switch -exact -- [dict get $dict game] 1 {
    if {[dict get $dict minutes] == 0 && [dict get $dict hours] == 0} {
      $window.error configure \
          -text "⚠ [@ $locale labels hours] or [
            @ $locale labels minutes] should be set"
    } elseif {[dict get $dict minutes] >= 60} {
      $window.error configure \
          -text "⚠ [@ $locale labels minutes] should be less than 60"
    } elseif {[dict get $dict clears] == 0} {
      $window.error configure \
          -text "⚠ [@ $locale labels clears] should be greater than 0"
    }
  } 2 {
    if {([dict get $dict titles] & (1 << 6)) == 0} {
      $window.error configure \
          -text "⚠ [@ $locale titles 2 \#6] should be set"
    }
  } 3 {
  } 4 {
    if {[dict get $dict name-solar] == ""} {
      $window.error configure \
          -text "⚠ [@ $locale labels name-solar] should be set"
    } elseif {[dict get $dict name-dark] == ""} {
      $window.error configure \
          -text "⚠ [@ $locale labels name-dark] should be set"
    }
  }
  if {[dict exists $dict name] && [dict get $dict name] == ""} {
    $window.error configure -text "⚠ [@ $locale labels name] should be set"
  }
  $window.password.entry delete 0 end
  $window.password.entry insert 0 [spaceout $pass]
}

# bok::ui::decode decodes the current password into the appropriate widgets.
# $window is the window initialized with bok::ui::init, $game is the game that
# $window is for, and $locale is the locale of bok::strings to use.
proc bok::ui::decode {window game {locale en}} {
  set region [region [$window.region-name get] $locale]
  set lang [expr {$region == "jp" ? "jp" : "en"}]
  if {[catch {
    set dict [decpass [$window.password.entry get] $game $lang]
  } error]} {
    $window.error configure -text "⚠ $error"
    return
  }
  if {[string match *!=* [dict get $dict checksum]]} {
    $window.error configure -text "⚠ checksum [dict get $dict checksum]"
  } elseif {[catch {normalize $dict $locale} error]} {
    $window.error configure -text "⚠ $error"
  } else {
    $window.error configure -text ✓
  }
  set dict [normalize $dict $locale 1]
  foreach {key list} [info $game $region $locale] {
    switch -exact -- [lindex $list 0] {} {continue} string {
      $window.$key delete 0 end
      $window.$key insert 0 [dict get $dict $key]
    } int - regionlist - list {
      $window.$key set [dict get $dict $key]
    } boolean {
      $window.$key state "[expr {[dict get $dict $key] ? "" : "!"}]selected"
    } bits {
      for {set i 0} {$i < [llength [lindex $list 1]]} {incr i} {
        $window.$key.$i state "[expr {
          [lindex $list 1 $i] in [dict get $dict $key] ? "" : "!"
        }]selected"
      }
    } hidden {
      set ::$window.$key [dict get $dict $key]
    }
  }
}

# bok::ui::cycleoffset increments the offset bit in the current password by
# $count, if it is present.  $window is the window initialized with
# bok::ui::init and $game is the game that $window is for.
proc bok::ui::cycleoffset {window game {count 1}} {
  set list [decb64 [$window.password.entry get] $game]
  set ok 0
  modkeys list $game jp 6 {k v n} offset region {
    if {$k == "region"} {set ok 1; return}
    expr {$v + $count & (1 << $n) - 1}
  }
  if {!$ok} {return}
  $window.password.entry delete 0 end
  $window.password.entry insert 0 [spaceout [encb64 $list $game]]
}

# bok::ui::keyboards is a dictionary of lists.  The lists contain lists of rows
# of keys, with the keys being in the form {display ?value? ?expand? ?width?}.
# The value defaults to the same as the display value, if the display is empty
# then that button is skipped, if "expand" is present then that key is expanded
# to take up horizontal space, and if "expand" has a width then that key is
# expanded to the specified width.
set bok::ui::keyboards {en64 {
  {B C D F G H J  @ # ^ {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  {K L M N P Q R  0 1 2 3 4}
  {S T V W X Y Z  5 6 7 8 9}
  {b c d f g h j  + - / .}
  {k l m n p q r  = _ ? {← <<PrevChar>>} {→ <<NextChar>>}}
  {s t v w x y z  : > ! {␣ { } expand}}
} lk64 {
  {1 2 3 4 5 6 7 8 9 0 . {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  {A B C D E F G H I J K L M}
  {N O P Q R S T U V W X Y Z}
  {a b c d e f g h i j k l m}
  {n o p q r s t u v w x y z}
  {? = {␣ { } expand} {← <<PrevChar>>} {→ <<NextChar>>}}
} jp64 {
  {あ い う え お  ま み む め も {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  {か き く け こ  や ゆ よ わ を ば び}
  {さ し す せ そ  ら り る れ ろ ぶ べ}
  {た ち つ て と  が ぎ ぐ げ ご ぼ}
  {な に ぬ ね の  ざ じ ず ぜ ぞ {← <<PrevChar>>} {→ <<NextChar>>}}
  {は ひ ふ へ ほ  だ ぢ づ で ど {␣ { } expand}}
} en {
  {A B C D E F G H I {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  {J K L M N O P Q R . ,}
  {S T U V W X Y Z {} ' -}
  {a b c d e f g h i /}
  {j k l m n o p q r {← <<PrevChar>>} {→ <<NextChar>>}}
  {s t u v w x y z {} {␣ { } expand}}
} ext {
  {& * ; + - × ÷ = {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar}}
  {à á â ä è é ê ë ( )}
  {ì í î ï ò ó ô ö [ ]}
  {ù ú û ü ç ñ œ ß ! ?}
  {À Á Â Ä È É Ê Ë ¡ ¿}
  {Ì Í Î Ï Ò Ó Ô Ö {← <<PrevChar>>} {→ <<NextChar>>}}
  {Ù Ú Û Ü Ç Ñ Œ {⋅ ⋅} {␣ { } expand}}
} hiri {
  {あ い う え お  ら り る れ ろ {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  {か き く け こ  が ぎ ぐ げ ご  ん ・}
  {さ し す せ そ  ざ じ ず ぜ ぞ  ー ~}
  {た ち つ て と  だ ぢ づ で ど}
  {な に ぬ ね の  ば び ぶ べ ぼ}
  {は ひ ふ へ ほ  ぱ ぴ ぷ ぺ ぽ}
  {ま み む め も  ぁ ぃ ぅ ぇ ぉ  {← <<PrevChar>>} {→ <<NextChar>>}}
  {や ゆ よ わ を  っ ゃ ゅ ょ {}  {␣ { } expand}}
} kata {
  {ア イ ウ エ オ  ラ リ ル レ ロ {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  {カ キ ク ケ コ  ガ ギ グ ゲ ゴ  ン ・}
  {サ シ ス セ ソ  ザ ジ ズ ゼ ゾ  ー ~}
  {タ チ ツ テ ト  ダ ヂ ヅ デ ド}
  {ナ ニ ヌ ネ ノ  バ ビ ブ ベ ボ}
  {ハ ヒ フ ヘ ホ  パ ピ プ ペ ポ}
  {マ ミ ム メ モ  ァ ィ ゥ ェ ォ {← <<PrevChar>>} {→ <<NextChar>>}}
  {ヤ ユ ヨ ワ ヲ  ッ ャ ュ ョ ヴ  {␣ { } expand}}
}}

# bok::ui::keyboardinit initializes a grid of buttons that send
# <<VirtualKeyPress>> events to the frame $window with %d set to the value.
# The default binding is to call bok::ui::keypress with %d.  Supported options
# are -style and -command.  -style sets the style to use for the buttons, and
# -command replaces the default binding for <<VirtualKeyPress>>.
proc bok::ui::keyboardinit {window keyboard args} {
  variable keyboards
  set usage {keyboardinit path keyboard ?option value ...?}
  set options [dict create -style {} \
      -command [list [namespace current]::keypress %W %d]
  ]
  if {[llength $args] % 2 != 0} {
    return -code error "wrong # args: should be \"$usage\""
  }
  if {![winfo exists $window]} {
    ttk::frame $window
  } elseif {[winfo exists $window.0]} {
    return
  }
  foreach {opt arg} $args {
    switch -exact -- $opt -keys - -command {
      dict set options $opt $arg
    } -style {
      dict lappend options $opt {*}$arg
    } default {
      return -code error "Invalid option: $opt"
    }
  }
  if {[dict exists $keyboards $keyboard]} {
    set keyboard [dict get $keyboards $keyboard]
  }
  bind $window <<VirtualKeyPress>> [dict get $options -command]
  set einfo {}
  set winfo {}
  foreach list $keyboard {
    set e 0
    set j 0
    foreach key $list {
      set width 1
      foreach opt [lrange $key 2 end] {
        if {[lindex $opt 0] != "expand"} {continue}
        set width [lindex $opt 1]
      }
      if {$width == ""} {
        set width 1
        incr e
      }
      incr j $width
    }
    lappend einfo $e
    lappend winfo $j
  }
  set max [::tcl::mathfunc::max {*}$winfo]
  set button 0
  for {set row 0} {$row < [llength $keyboard]} {incr row} {
    set list [lindex $keyboard $row]
    set n [lindex $einfo $row]
    set e [expr {max($max - [lindex $winfo $row] + $n - 1, 0)}]
    set elist {}
    if {$n > 0} {
      for {set i 0; set j [expr {int(($n - $e % $n) / 2)}]} {$i < $n} {incr i} {
        lappend elist [expr {
          int($e / $n) + ($i >= $j && $i < $n - $j ? 1 : 0)
        }]
      }
    }
    for {set e 0; set i 0; set col 0} {$i < [llength $list]} {incr i} {
      set key [lindex $list $i]
      if {$key == ""} {
        incr col
        continue
      }
      if {[llength $key] < 2} {
        set text $key
        set value $key
      } else {
        lassign $key text value
      }
      set width 1
      foreach opt [lrange $key 2 end] {
        switch -nocase -exact -- [lindex $opt 0] expand {
          set width [lindex $opt 1]
        }
      }
      if {$width == ""} {
        set width [lindex $elist $e]
        incr e
      }
      grid [ttk::button $window.$button -text $text -command \
          [list event generate $window <<VirtualKeyPress>> -data $value \
              -root [winfo toplevel $window] -subwindow $window.$button] \
          -style [dict get $options -style] -width 2 -takefocus 0
      ] -ipadx 0 -row $row -column $col -columnspan $width -sticky nsew
      incr button
      incr col $width
    }
  }
  for {set row 0} {$row < [llength $keyboard]} {incr row} {
    grid rowconfigure $window $row -weight 1
  }
  for {set col 0} {$col < $max} {incr col} {
    grid columnconfigure $window $col -weight 1
  }
  return $window
}

# bok::ui::keypress inserts $value into Entry/TEntry currently in focus for the
# toplevel window for $window.  If value is a virtual event then that action is
# performed instead.  Supported virtual events are PrevChar, NextChar,
# DeletePrevChar, and DeleteNextChar.
proc bok::ui::keypress {window value} {
  set window [focus -lastfor [winfo toplevel $window]]
  switch -glob -nocase -- [winfo class $window] *Entry {
    switch -glob -nocase -- $value {
    <<PrevChar>> {$window icursor [expr {[$window index i] - 1}]}
    <<NextChar>> {$window icursor [expr {[$window index i] + 1}]}
    <<DeletePrevChar>> {$window delete [expr {[$window index i] - 1}]}
    <<DeleteNextChar>> {$window delete i}
    <<*>> {return -code error "Unsupported event: $value"}
    default {
      $window insert i $value
    }}
  }
}

# bok::ui::keyboardallow disables/enables all keyboards in $window depending on
# if they are in $keyboards.  The tab is set to the default (or first valid
# keyboard) if the current keyboard is not in $keyboards.
proc bok::ui::keyboardallow {window keyboards {default ""}} {
  if {$default == "" || $default ni $keyboards} {
    set default [lindex $keyboards 0]
  }
  set current [winfo name [$window select]]
  foreach keyboard [$window tabs] {
    set name [winfo name $keyboard]
    $window tab $keyboard \
        -state [expr {$name in $keyboards ? "normal" : "disabled"}]
    if {$name == $default && $current ni $keyboards} {
      $window select $keyboard
    }
  }
}

proc usage {} {
  puts stderr [join {
    {usage: bokpass [-gui] [-kbd] [-invalid] [-columns columns]}
    {               [-fontsize size]}
    {       bokpass [-cli] [-verbose] -game game password ...}
  } "\n"]
  exit 1
}

set options {
  -locale en
  -mode cli -verbose 0
  -columns 2 -fontsize 10 -kbd 0 -validate all
}
for {set optind 0} {$optind < [llength $argv]} {incr optind} {
  switch -glob -- [lindex $argv $optind] -game - -columns - -fontsize {
    if {$optind + 1 >= [llength $argv]} {usage}
    dict set options {*}[lrange $argv $optind $optind+1]
    incr optind
  } -cli - -gui {
    dict set options -mode [string range [lindex $argv $optind] 1 end]
  } -verbose - -kbd {
    dict set options [lindex $argv $optind] 1
  } -invalid {
    dict set options -validate none
  } -- {
    incr optind
    break
  } -?* {
    usage
  } default {
    break
  }
}
if {$optind == [llength $argv]} {
  dict set options -mode gui
}
if {[dict get $options -mode] == "cli" && ![dict exists $options -game]} {
  usage
}

switch -exact -- [dict get $options -mode] gui {
  package require Tk

  wm title . bokpass

  ttk::style configure Keyboard.TButton \
      -font "mono [expr {int([dict get $options -fontsize] * 1.2)}]"
  ttk::style configure TLabel \
      -font "sans [dict get $options -fontsize]"
  ttk::style configure TNotebook.Tab \
      -font "sans [dict get $options -fontsize]"
  ttk::style configure TSpinBox -font "sans [dict get $options -fontsize]"
  ttk::style configure TComboBox -font "sans [dict get $options -fontsize]"
  option add *TCombobox*Listbox.font "sans [dict get $options -fontsize]"

  # Adds ^W and ^U functionality to Entry and TEntry widgets.
  proc betterediting {args} {
    foreach arg $args {
      bind $arg <Control-KeyPress-w> {
        apply {old {
          event generate %W <<PrevWord>>
          %W delete [%W index i] $old
        }} [%W index i]
      }
      bind $arg <Control-KeyPress-u> {
        %W delete 0 end
      }
      bind $arg <KeyPress-Up> {%W icursor 0}
      bind $arg <KeyPress-Down> {%W icursor e}
    }
  }
  betterediting Entry TEntry

  # Lazily initializes game tabs.
  proc loadgametab {window} {
    if {[dict get $::options -kbd]} {
      foreach keyboard [.keyboard tabs] {
        .keyboard tab $keyboard -state normal
      }
    }
    if {[winfo exists $window.password]} {return}
    if {![string match {*.*[1234]} $window]} {return}
    set game [string index $window end]
    set bindings {}
    if {[dict get $::options -kbd]} {
      lappend bindings password.entry {<FocusIn> {+
        if {[bok::ui::region \
            [[winfo parent [winfo parent %W]].region-name get] \
            [dict get $::options -locale]] == "jp"} {
          bok::ui::keyboardallow .keyboard jp64
        } elseif {[string index [.games select] end] == "4"} {
          bok::ui::keyboardallow .keyboard lk64
        } else {
          bok::ui::keyboardallow .keyboard en64
        }
      }} string {<FocusIn> {+
        switch -glob -- "[bok::ui::region [[winfo parent %W].region-name get] \
            [dict get $::options -locale]][string index [.games select] end]" {
        jp[123] {
          bok::ui::keyboardallow .keyboard {hiri kata} hiri
        } jp4 {
          bok::ui::keyboardallow .keyboard {en ext hiri kata} hiri
        } na[123] - eu[123] {
          bok::ui::keyboardallow .keyboard {en}
        } na4 - eu4 {
          bok::ui::keyboardallow .keyboard {en ext hiri kata} en
        }}
      }} entry {<FocusOut> {
        bok::ui::keyboardallow .keyboard {en64 lk64 jp64 en ext kata hiri}
      }}
    }
    if {$game != 3} {
      switch -exact -- [dict get $::options -locale] {
      en {set region na}
      jp {set region jp}
      default {set region eu}
      }
    } else {
      set region jp
    }
    set options [dict map {key value} $::options {
      if {$key ni {-fontsize -columns -validate}} {continue}
      set value
    }]
    bok::ui::init $window $region $game -bind $bindings {*}$options
  }
  pack [ttk::notebook .games] -expand 1 -fill both
  bind .games <<NotebookTabChanged>> {loadgametab [%W select]}
  foreach {tab name} [bok::@ [dict get $options -locale] labels game-tabs] {
    .games add [ttk::frame .games.$tab] -text $name
  }

  if {[dict get $options -kbd]} {
    # Lazily initializes keyboard tabs.
    pack [ttk::notebook .keyboard -takefocus 0] -fill both
    bind .keyboard <<NotebookTabChanged>> {
      if {![winfo exists [%W select].0]} {
        bok::ui::keyboardinit [%W select] [winfo name [%W select]] \
            -style Keyboard.TButton
      }
    }
    bind Entry <FocusIn> {+ set ::currententry %W}
    bind TEntry <FocusIn> {+ set ::currententry %W}
    bind .games <<NotebookTabChanged>> {+ set ::currententry {}}
    bind .keyboard <<NotebookTabChanged>> {+
      if {[winfo exists $::currententry]} {focus $::currententry}
    }
    foreach {tab name} \
        [bok::@ [dict get $options -locale] labels keyboard-tabs] {
      .keyboard add [ttk::frame .keyboard.$tab] -text $name
    }
  }
} cli {
  # Forgivingly match -game.
  switch -exact -nocase -- [regsub {[^[:alnum:]]+} \
      [dict get $options -game] {}] {
  boktai - tsiiyh - boktai1 - boktaitsiiyh - boktaithesunisinyourhand -
  bokura - bokura1 - bokuranotaiyou - bokuranotaiyou1 -
  jp1 - en1 - na1 - eu1 - us1 - 1 {
    dict set options -game 1
  }
  zoktai - boktai2 - boktai2solarboydjango - solarboydjango -
  zokubokura - zokubokuranotaiyou - bokura2 - bokuranotaiyou2 -
  jp2 - en2 - na2 - eu2 - us2 - 2 {
    dict set options -game 2
  }
  shinbok - boktai3 - boktai3sabatascounterattack - sabatascounterattack -
  shinbokura - shinbokuranotaiyou - bokura3 - bokuranotaiyou3 -
  jp3 - en3 - na3 - eu3 - us3 - 3 {
    dict set options -game 3
  }
  lk - lunarknigths - boktai4 - boktaids - bokura4 - bokurads -
  bokuranotaiyouds - bokuranotaiyou4 - boktaidjangosabata - bokuradjangosabata -
  bokuranotaiyoudjangosabata - djangosabata - ds -
  jp4 - en4 - na4 - eu4 - us4 - 4 {
    dict set options -game 4
  } default {
    puts stderr "Unknown game: [dict get $options -game]"
    exit 1
  }}

  # Decodes passwords for -game and print them in columns.
  set dict [dict create]
  for {} {$optind < [llength $argv]} {incr optind} {
    if {[catch {
      set width [::tcl::mathfunc::max 0 {*}[lmap value [dict values $dict] {
        string length $value
      }]]
      set tdict [bok::decpass [lindex $argv $optind] \
          [dict get $options -game]]
      set vdict {}
      catch {set vdict [bok::normalize $tdict [dict get $options -locale] 1]}
      dict for {key value} $tdict {
        set pad {}
        if {[dict exists $dict $key]} {
          set pad [string repeat { } [expr {
            $width - [string length [dict get $dict $key]]
          }]]
        }
        if {[dict exists $vdict $key]} {
          set value [dict get $vdict $key]
        }
        if {[dict get $options -verbose] && [dict exists $vdict $key\-name]} {
          append value " \"[dict get $vdict $key\-name]\""
        }
        dict append dict $key " $pad$value"
      }
    } error]} {
      puts stderr "bokpass: $error"
    }
  }
  set width [::tcl::mathfunc::max 0 {*}[lmap key [dict keys $dict] {
    string length $key
  }]]
  dict for {key value} $dict {
    set pad [string repeat { } [expr {$width - [string length $key]}]]
    puts $pad$key:$value
  }
}
